# 自动求梯度
# 导入模块
import torch

# 如果将Tensor属性.requires_grad设置为True，默认为False，它将开始追踪(track)在其上的所有操作（这样就可以利用链式法则进行梯度传播了）
x = torch.ones(2, 2, requires_grad=True)
print(x)
print(x.grad_fn)
y = x + 2
print(y)
# 输出<AddBackward0 object at 0x0000028C9D598550>，表明y是经过一个addBackward得到的
print(y.grad_fn)
# 像 x 这种直接创建的称为叶子节点，叶子节点对应的grad_fn是None
print(x.is_leaf, y.is_leaf)
# 更复杂的一个计算，这里z相当于3(x+2)²
z = y * y * 3
out = z.mean()
print(z, out)
print(z.grad_fn)

# 通过.requires_grad_()来用in-place（就地）的方式改变requires_grad属性
a = torch.ones(2, 2)
a = ((a * 3) / (a - 1))
print(a)
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.grad_fn)

# out必须是一个标量，否则需要传入一个中间参数，见书上解析，计算out关于自变量的梯度（本处是只有x，但实际上可能是多元函数，不一定只有x，也可能有其他变量，不但需要是叶子节点)
out.backward()
# 输出out关于x的梯度
print("第一次求out关于x的梯度：")
print(x.grad)

# 关于同一个自变量的梯度是累加的
out2 = x.sum()
out2.backward()
print("累加到二次求out关于x的梯度：")
# 这次是1，之前4.5，总的为5.5
print(x.grad)

# 梯度清0
out3 = x.sum()
# 清0
x.grad.data.zero_()
# 重新计算
out3.backward()
print("清0之后的新梯度：")
print(x.grad)

x = torch.tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True)
y = 2 * x
z = y.view(2, 2)
print(z)

# z并不是一个标量，使用backward()需要传入一个和z同型的权重向量,进行加权求和得到一个标量
v = torch.tensor([[1.0, 0.1], [0.01, 0.001]], dtype=torch.float)
z.backward(v)
# x.grad是x的同型的张量
print(x.grad)

# 中断梯度追踪
x = torch.tensor(1.0, requires_grad=True)
y1 = x ** 2
# 此外，还可以用with torch.no_grad()将不想被追踪的操作代码块包裹起来。对这块代码不计入计算梯度的范围内
with torch.no_grad():
    y2 = x ** 3
y3 = y1 + y2

print(x.requires_grad)
print(y1, y1.requires_grad)
# y2.requires_grad=False，所以不能调用 y2.backward()
print(y2, y2.requires_grad)
print(y3, y3.requires_grad)

# y3对x求梯度不考虑y2，因为y2的内容被 with torch.no_grad() 包裹了
# y3是标量，不用传入参数
y3.backward()
# 结果是2，而不是5
print(x.grad)

# 如果我们想要修改tensor的数值，但是又不希望被autograd记录（即不会影响反向传播），那么我么可以对tensor.data进行操作
x = torch.ones(1, requires_grad=True)
# x.data还是一个tensor
print(x.data)
# 但是x.data已经独立于计算图之外了
print(x.data.requires_grad)

y = 2 * x
# 只改变了值，不会记录在计算图，所以不会影响梯度传播，但tensor x 的值也已经变了
x.data *= 100

y.backward()
print(x)
print(x.grad)
